# Clint Banis Copyright 2010 All rights reserved.
# ---
#
# FB Login auth part based on facebookoauth.py,
# with cookie-processing stolen verbatim:
#
#   Facebook:
#   http://github.com/facebook/python-sdk/blob/master/examples/oauth/facebookoauth.py
#   http://www.apache.org/licenses/LICENSE-2.0
#
from google.appengine.api import memcache
from facebook.models import FacebookUser, FacebookAppSetting

from urllib import urlencode, urlopen, splitquery, quote_plus, unquote_plus, splittype, splithost
from cgi import parse_qsl
from django.utils import simplejson as json

import logging
import base64
import hmac
import Cookie
import time
import hashlib
import email

def getApplicationSetting(name, default = None):
    q = FacebookAppSetting.all()
    q.filter('name', name)

    for setting in q.fetch(1):
        return setting.value

    return default

def setApplicationSetting(name, value):
    q = FacebookAppSetting.all()
    q.filter('name', name)

    for setting in q.fetch(1):
        break
    else:
        setting = FacebookAppSetting(name = name)

    setting.value = value
    setting.put()

FBAppID             = 'FBAppID'
FBAppSecret         = 'FBAppSecret'

FBAppAccessTokenUrl = 'FBAccessTokenUrl'
FBAppOAuthUrl       = 'FBAppOAuthUrl'
FBAppUserProfileUrl = 'FBAppUserProfileUrl'

# FBAppSettingsKeys = [FBAppID, FBAppSecret, FBAppAccessTokenUrl,
#                      FBAppOAuthUrl, FBAppUserProfileUrl]

FBAppCacheKey = 'fb-app-settings'

ACCESS_TOKEN_URL = 'https://graph.facebook.com/oauth/access_token'
OAUTH_URL        = 'https://graph.facebook.com/oauth/authorize'
USER_PROFILE_URL = 'https://graph.facebook.com/me'

_settings_symbols = [('app_id', FBAppID, ''), ('app_secret', FBAppSecret, ''),
                     ('access_token_url', FBAppAccessTokenUrl, ACCESS_TOKEN_URL),
                     ('oauth_url'       , FBAppOAuthUrl      , OAUTH_URL       ),
                     ('user_profile_url', FBAppUserProfileUrl, USER_PROFILE_URL)]

class Settings(dict):
    def __getattr__(self, name):
        try: return self[name]
        except KeyError:
            return super(dict, self).__getattr__(name)

    def __iter__(self):
        get = super(dict, self).get
        for (name, _, default) in _settings_symbols:
            return get(name, default)

    class Symbol:
        def __init__(self, name, symbol, value, is_default):
            self.name = name
            self.symbol = symbol
            self.value = value
            self.is_default = is_default

    def symbolic(self):
        for (name, symbol, default) in _settings_symbols:
            try: yield self.Symbol(name, symbol, self[name], False)
            except KeyError:
                yield self.Symbol(name, symbol, default, True)

def getFBAppSettings(SettingsClass = Settings):
    settings = memcache.get(FBAppCacheKey)
    if settings is None:
        settings = dict((name, getApplicationSetting(symbol, default)) \
                        for (name, symbol, default) in _settings_symbols)

        memcache.set(FBAppCacheKey, settings)

    return SettingsClass(settings)

_undefined = object()
def setFBAppSettings(**settings):
    # todo: conform to Settings class..
    for (name, symbol, _) in _settings_symbols:
        value = settings.get(name, _undefined)
        if value is not _undefined:
            setApplicationSetting(symbol, value)

    memcache.set(FBAppCacheKey, settings)

class InvalidSignature(Exception):
    pass

def verify_fb_app_req(handler):
    '''
    // http://developers.facebook.com/docs/authentication/

    $args = array();
    parse_str(trim($_COOKIE['fbs_' . $app_id], '\\"'), $args);
    ksort($args);

    $payload = '';
    foreach ($args as $key => $value) {
        if ($key != 'sig') {
            $payload .= $key . '=' . $value;
        }
    }
    if (md5($payload . $application_secret) != $args['sig']) {
      return null;
    }
    return $args;
    '''

    # Query datastore for app_id, application_secret.
    # (Actually, the app_id could come from the request..)
    app_id = getApplicationSetting('FBAppID') or 0
    application_secret = getApplicationSetting('FBAppSecret') or ''

    # Extract request payload.
    (payload, sigdigest, sig) = get_fb_sig_data(handler, app_id)
    payload += application_secret

    # Evaluate verification and return data if possible.
    if get_fb_sig_checksum(payload, sigdigest):
        return sig

    raise InvalidSignature

def get_fb_sig_data(handler, app_id):
    # Obtain signature dictionary object.
    sig = handler.request.cookies.get('fbs_%s' % app_id)
    if sig is not None:
        # Parse as query string.
        sig = sig.replace('\\', '').replace('"')
        sig = dict(x.split('=') for x in sig.split('&'))

        sigdigest = sig.pop('sig')

    else:
        # Use any other parameters is cookie not set.
        p = handler.request.params

        ln = len('fb_sig_')
        sig = dict((n[ln:], p.get(n)) for n in p.keys() \
                    if n.startswith('fb_sig_'))

        sigdigest = p.get('fb_sig')

    # Signature order:
    keys = sig.keys()
    keys.sort()

    # Presumably, values do not need further encoding...
    payload = ''.join('%s=%s' % (n, sig[n]) for n in keys)
    return (payload, sigdigest, sig)

def get_fb_sig_checksum(data, sig):
    import hashlib
    return hashlib.md5(data).hexdigest() == sig

    ##    import hmac
    ##    return hmac.new(data, digestmod = hashlib.sha1).hexdigest() == sig

class FacebookAppRequest:
    # Objectifies the facebook app/user data for other plugins.
    # Verification occurs when the passed user signature
    #    is used to procure permissible user data.  If the user
    #    didn't really authorize the application, FB will deny
    #    access to this user (and so shall we).

    def __init__(self, handler):
        self._sig = verify_fb_app_req(handler)
        # or, cookie packet

        self._handler = handler
        handler.fb_app_req = self

        fb_sig_user = self._getUserId()
        if fb_sig_user is not None:
            self.fb_user = self._getUser(fb_sig_user) or \
                self._createUser(fb_sig_user)

    def _getUserId(self):
        fb_sig_user = self._handler.request.get('fb_sig_user')
        if type(fb_sig_user) is str and not fb_sig_user.isdigit():
            return

        # Cast for all other types.
        return int(fb_sig_user)

    def _getUser(self, fb_sig_user):
        return getFBUserByID(fb_sig_user)

    def _createUser(self, fb_sig_user):
        fb_user = FacebookUser(user_id = fb_sig_user)
        fb_user.put()
        return fb_user

    def _get_sig_parts(self):
        # Return a sequence suitable for urlencode, which expects a
        # primitive sequence (apparently a generator/dict-listiterator
        # isn't sufficient).
        return [(name, value) for (name, value) in \
                self._handler.request.params.iteritems() \
                if name.startswith('fb_sig')]

    def query(self):
        return dict(self._get_sig_parts())

    #@property
    def signature(self):
        try: return self._sig_string
        except AttributeError:
            sig = self._sig_string = urlencode(self._get_sig_parts())
            return sig

def getFBAppReq(handler):
    try: return handler.fb_app_req
    except AttributeError:
        return FacebookAppRequest(handler)

def getFBUserByID(user_id, create = True):
    q = FacebookUser.all()
    q.filter('user_id = ', int(user_id))

    for fb_user in q.fetch(1):
        return fb_user

    if create:
        return FacebookUser()

# Authentication for querying user data.
class LoginWare(Settings):
    # Utilizes facebook settings to mask dev_appserver as fb auth server.
    def __init__(self):
        super(LoginWare, self).__init__(getFBAppSettings())

    def _get_access_token(self, redirect_uri, verif_code):
        args = dict(client_id = self.app_id, redirect_uri = redirect_uri)

        if verif_code:
            args["client_secret"] = self.app_secret
            args["code"] = verif_code

            url = rebuild_query(self.access_token_url, args)
            response = simple_parse_qs(urlopen(url).read())

            try: return response['access_token']
            except KeyError:
                return None

    def _get_user_data(self, access_token):
        args = dict(access_token = access_token)
        url = '%s?%s' % (self.user_profile_url, urlencode(args))
        return json.load(urlopen(url))

    def _perform_user_login(self, redirect_uri, verif_code):
        access_token = self._get_access_token(redirect_uri, verif_code)
        if access_token is None:
            args = dict(client_id = self.app_id, redirect_uri = redirect_uri)
            return rebuild_query(self.oauth_url, args)

        return (access_token, self._get_user_data(access_token))

    def login_handler(self, handler, redirect_uri = None):
        if redirect_uri is None:
            redirect_uri = handler.request.path_url

        response = self._perform_user_login(redirect_uri, handler.request.get("code"))

        if isinstance(response, basestring):
            handler.redirect(response)
            return (None, None)

        return response

    def get_current_user(self, handler):
        try: return self._current_user
        except AttributeError:
            user_id = self._parse_cookie(handler.request.cookie.get('fb_user'))
            if user_id:
                user = self._current_user = FacebookUser.get_by_key_name(user_id)
                return user

    # facebookoauth.py:
    def _set_cookie(self, response, name, value, domain=None, path="/", expires=None):
        """Generates and signs a cookie for the give name/value"""
        timestamp = str(int(time.time()))
        value = base64.b64encode(value)
        signature = self._cookie_signature(value, timestamp)
        cookie = Cookie.BaseCookie()
        cookie[name] = "|".join([value, timestamp, signature])
        cookie[name]["path"] = path
        if domain: cookie[name]["domain"] = domain
        if expires:
            cookie[name]["expires"] = email.utils.formatdate(
                expires, localtime=False, usegmt=True)
        response.headers._headers.append(("Set-Cookie", cookie.output()[12:]))

    def _parse_cookie(self, value):
        """Parses and verifies a cookie value from set_cookie"""
        if not value: return None
        parts = value.split("|")
        if len(parts) != 3: return None
        if self._cookie_signature(parts[0], parts[1]) != parts[2]:
            logging.warning("Invalid cookie signature %r", value)
            return None
        timestamp = int(parts[1])
        if timestamp < time.time() - 30 * 86400:
            logging.warning("Expired cookie %r", value)
            return None
        try:
            return base64.b64decode(parts[0]).strip()
        except:
            return None

    def _cookie_signature(self, *parts):
        """Generates a cookie signature.

        We use the Facebook app secret since it is different for every app (so
        people using this example don't accidentally all use the same secret).
        """
        hash = hmac.new(str(self.app_secret), digestmod=hashlib.sha1)
        for part in parts: hash.update(part)
        return hash.hexdigest()

from facebook.page import FacebookApplication
class FBAppHandler(FacebookApplication):
    def get(self):
        # Prepare transient args: these get encoded into redirect_uri to be passed through
        # the cross-domain apparatus.
        args = dict()

        after_login = self.request.params.get('after_login')
        if after_login is not None:
            args['after_login'] = after_login

        fb_sig_encoded = self.request.params.get('fb_sig_encoded')
        if fb_sig_encoded is not None:
            args['fb_sig_encoded'] = fb_sig_encoded

        # XXX Doesn't necessarily have to validate...
        try: fb_app_req = getFBAppReq(self)
        except InvalidSignature: pass
        else: args['fb_sig_encoded'] = quote_plus(fb_app_req.signature())

        # Now perform the encoding.
        if args:
            (proto, host_query) = splittype(self.request.path_url)
            (host, path_query) = splithost(host_query)

            # Rebuild.
            redirect_uri = '%s://%s%s' % (proto, host, rebuild_query(path_query, args))

        ware = LoginWare()
        (access_token, profile) = ware.login_handler(self, redirect_uri = redirect_uri)

        if access_token is not None:
            # Skip signed query, access user data from profile info directly!
            user = getFBUserByID(profile['id'], create = True)

            user.access_token = access_token
            user.name = profile['name']
            user.profile_url = profile['link']
            user.put()

            # And then..?  Cookies on user_id and claimant_id:
            # (well, not claimant_id, that can piggyback other cookies in its proper)
            ware._set_cookie(self.response, "fb_user", str(profile["id"]),
                             expires=time.time() + 30 * 86400)

            if after_login:
                if fb_sig_encoded:
                    fb_sig = simple_parse_qs(unquote_plus(fb_sig_encoded))
                    after_login = rebuild_query(after_login, fb_sig)

                return self.redirect(after_login)

            # This just shows page status:
            return FacebookApplication.get(self, path = '')

def simple_parse_qs(qs):
    # Evaluate only last value for each key-pair parameter.
    r = dict()
    for (name, value) in parse_qsl(qs):
        r[name] = value

    return r

def rebuild_query(original, args):
    # Reduce original query string to dictionary, update with existing
    # dictionary, then rebuild new path with query string.

    (path, query) = splitquery(original)
    if query is None:
        query = args
    else:
        query = simple_parse_qs(query)
        query.update(args)

    return '%s?%s' % (path, urlencode(query))

# Ancillary Functionality to Authentication:
MESSAGE_URL = 'https://graph.facebook.com/%(user_id)d/feed'

def post_message(user, message):
    url = MESSAGE_URL % dict(user_id = user.user_id)
    args = dict(message = message,
                access_token = user.access_token)

    url = '%s?%s' % (url, urlencode(args))
    return json.load(urlopen(url))
